Skip to content

✨ Implement controlled unitary embedding#1822

Closed
simon1hofmann wants to merge 10 commits into
mainfrom
fix/ctrl-op-controlled-embed
Closed

✨ Implement controlled unitary embedding#1822
simon1hofmann wants to merge 10 commits into
mainfrom
fix/ctrl-op-controlled-embed

Conversation

@simon1hofmann

Copy link
Copy Markdown
Contributor

Description

Fixes incorrect controlled-gate unitary matrices in QCO and unifies matrix wire indexing on the QuantumComputation convention.

Problem: CtrlOp::getUnitaryMatrix() embedded the inner unitary via DynamicMatrix::setBottomRightCorner (I_{2^c} ⊗ U), which is only correct when controls sit on the most significant wires. For general wire placement (e.g. cx(q[0], q[1])), the result did not match QuantumComputation.

Solution:

  • Add embedControlledUnitary() to place a multi-controlled unitary using actual control/target register indices (QC little-endian: qubit i = bit i).
  • Resolve compile-time wire indices in CtrlOp from qco.static and constant qtensor.extract operands.
  • Unify embedInNqubit, kron, and related helpers on the same bit ordering (replacing the old MSB-first embedding).
  • Remove unused DynamicMatrix::setBottomRightCorner API.

Fixes #1821

Checklist

  • The pull request only contains commits that are focused and relevant to this change.
  • I have added appropriate tests that cover the new/changed functionality.
  • I have updated the documentation to reflect these changes.
  • I have added entries to the changelog for any noteworthy additions, changes, fixes, or removals.
  • I have added migration instructions to the upgrade guide (if needed).
  • The changes follow the project's style guidelines and introduce no new warnings.
  • The changes are fully tested and pass the CI checks.
  • I have reviewed my own code changes.

If PR contains AI-assisted content:

  • I have disclosed the use of AI tools in the PR description as per our AI Usage Guidelines.
  • AI-assisted commits include an Assisted-by: [Model Name] via [Tool Name] footer.
  • I confirm that I have personally reviewed and understood all AI-generated content, and accept full responsibility for it.

@simon1hofmann simon1hofmann self-assigned this Jun 29, 2026
@simon1hofmann simon1hofmann added c++ Anything related to C++ code MLIR Anything related to MLIR labels Jun 29, 2026
@simon1hofmann simon1hofmann added this to the MLIR Support milestone Jun 29, 2026
@codecov

codecov Bot commented Jun 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.81443% with 6 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp 91.8% 4 Missing ⚠️
mlir/lib/Dialect/QCO/Utils/Matrix.cpp 95.8% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@munich-quantum-toolkit munich-quantum-toolkit deleted a comment from coderabbitai Bot Jun 30, 2026
…t indices and improve matrix embedding logic. Add unit test for sparse wire indices to ensure correctness.
@simon1hofmann

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added an embedControlledUnitary helper to embed a target unitary as a multi-controlled operation over specified control/target wire sets.
  • Bug Fixes
    • Updated controlled-unitary and general matrix embedding to use consistent qubit/wire bit ordering, improving results for non-adjacent and sparse wire layouts.
    • Corrected expected controlled-gate matrix conventions (e.g., CX, CH) and improved participating-qubit handling.
  • Breaking Changes
    • Removed several DynamicMatrix::setBottomRightCorner(...) overloads.
  • Tests
    • Expanded and updated unit and validation coverage for controlled embedding and wire-ordering rules.

Walkthrough

Updates QCO matrix wire/bit conventions, adds embedControlledUnitary, rewrites CtrlOp::getUnitaryMatrix() to resolve static qubit indices before embedding, and adjusts tests for the revised controlled-operator ordering and allocation behavior.

Changes

Register-aware controlled unitary embedding

Layer / File(s) Summary
Matrix API and wire-order docs
mlir/include/mlir/Dialect/QCO/Utils/Matrix.h
Revises embedding and Kronecker-product docs to use the QuantumComputation qubit/bit convention, removes setBottomRightCorner declarations, and adds embedControlledUnitary(...).
Matrix embedding and control construction
mlir/lib/Dialect/QCO/Utils/Matrix.cpp
Changes basis-bit extraction to direct qubit indexing, refactors controlled-unitary embedding around target subspace packing and wire masks, updates one- and two-qubit embedding helpers, and removes bottom-right-corner implementations.
CtrlOp register resolution and embedding
mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp
Adds SSA-to-register index resolution helpers, then rewrites CtrlOp::getUnitaryMatrix() to resolve controls and targets, derive participating qubits, remap them locally, and call embedControlledUnitary.
Matrix utility tests
mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp
Replaces bottom-right-corner coverage with embedControlledUnitary correctness and failure cases, and updates Kronecker-product, reorder, and embedInNqubit expectations for the revised wire ordering.
CtrlOp matrix tests
mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp
Adds helpers for controlled-cx construction and optional matrix extraction, updates reference matrices for control/target ordering, and adds cases for static qubits, sparse wires, and invalid operand allocation.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

fix

Suggested reviewers

  • burgholzer
  • denialhaag

🐇 I hop through bits both near and far,
Now qubit maps match where they are.
No corner tricks, just clear control,
With wires aligned to one true goal.
The bunny grins: “All indices sing!”
And matrices do the right-end thing.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly reflects the main change: adding controlled unitary embedding.
Description check ✅ Passed The description includes a summary, motivation, context, and the fixed issue, with a checklist present.
Linked Issues check ✅ Passed The changes implement register-aware controlled embedding and distinguish operand order as required by #1821.
Out of Scope Changes check ✅ Passed The documentation updates, API removal, and test changes are all aligned with the controlled-embedding fix.
✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch fix/ctrl-op-controlled-embed

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp (1)

147-155: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Assert the optional matrix before dereferencing it.

getUnitaryMatrix() can return std::nullopt; this test should fail cleanly instead of dereferencing a disengaged optional if resolution regresses.

Proposed fix
   auto ctrlOp = *funcOp.getBody().getOps<CtrlOp>().begin();
   auto matrix = ctrlOp.getUnitaryMatrix();
+  ASSERT_TRUE(matrix);
 
   const Matrix4x4 expected =
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp` around lines 147 - 155,
The test around getUnitaryMatrix in test_qco_ir_matrix.cpp should not
dereference the optional directly, since ctrlOp.getUnitaryMatrix() may return
std::nullopt. Update the assertion flow to first verify the optional is engaged
before using matrix->isApprox(expected), so the test fails cleanly if unitary
matrix resolution regresses. Use the getUnitaryMatrix result variable and the
existing expectedMatrixFromComputation setup to keep the check in the same test
case.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@mlir/include/mlir/Dialect/QCO/Utils/Matrix.h`:
- Around line 745-747: Update the documentation for the matrix utility that
takes control and target qubits so the parameter comments clearly say these are
embedding-space/local wire indices used after remapping, not raw
program-register indices. Adjust the wording in the comment attached to the
relevant matrix application helper so callers like CtrlOp understand they must
pass the compact positions corresponding to numQubits/participating rather than
sparse global wire numbers.

In `@mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp`:
- Around line 48-49: Drop the top-level const from MLIR handle-type parameters
in the QCO modifiers API so the signatures match MLIR conventions. Update
programQubitIndex and any related declarations/definitions in CtrlOp.cpp (and
the corresponding header or callers if needed) to take Value and ValueRange
without const, while keeping the existing logic unchanged.
- Around line 386-391: The duplicate-removal logic in the participating qubit
list is wrong because `std::ranges::unique(participating).end()` returns the
original end iterator, so the erase does nothing and duplicates can still reach
`embedControlledUnitary`. Update the deduplication in `CtrlOp::` the block that
builds `participating` to erase the returned unique subrange itself, using the
result of `std::ranges::unique(participating)` rather than its `.end()` member,
so repeated wire indices are actually removed before validation.

In `@mlir/lib/Dialect/QCO/Utils/Matrix.cpp`:
- Around line 218-227: The validation in Matrix::embedUnitary currently only
checks wire ranges, so add explicit duplicate/overlap checks before building the
embedding. In the same section that validates controlQubits and targetQubits,
reject any repeated entries within targetQubits and any wire that appears in
both controlQubits and targetQubits, so the later mask construction cannot
silently collapse distinct operands. Keep the existing range asserts, but
tighten the preconditions in embedUnitary to fail fast on invalid control/target
wire sets.

---

Outside diff comments:
In `@mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp`:
- Around line 147-155: The test around getUnitaryMatrix in
test_qco_ir_matrix.cpp should not dereference the optional directly, since
ctrlOp.getUnitaryMatrix() may return std::nullopt. Update the assertion flow to
first verify the optional is engaged before using matrix->isApprox(expected), so
the test fails cleanly if unitary matrix resolution regresses. Use the
getUnitaryMatrix result variable and the existing expectedMatrixFromComputation
setup to keep the check in the same test case.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4c9588ee-b7fa-4990-a30d-6395baca21b2

📥 Commits

Reviewing files that changed from the base of the PR and between d5ffcf1 and 44e18e1.

📒 Files selected for processing (5)
  • mlir/include/mlir/Dialect/QCO/Utils/Matrix.h
  • mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp
  • mlir/lib/Dialect/QCO/Utils/Matrix.cpp
  • mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp
  • mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp

Comment thread mlir/include/mlir/Dialect/QCO/Utils/Matrix.h Outdated
Comment thread mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp Outdated
Comment thread mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp Outdated
Comment thread mlir/lib/Dialect/QCO/Utils/Matrix.cpp
@simon1hofmann

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp (2)

47-81: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add Doxygen comments for the new internal helpers.

These new C++ helpers have no header declarations where API docs can live, so add brief /// comments here.

Proposed documentation
+/// Returns the statically-known program qubit index for `@p` qubit, if available.
 [[nodiscard]] static std::optional<std::size_t> programQubitIndex(Value qubit) {
@@
+/// Resolves all `@p` qubits to statically-known program qubit indices.
 [[nodiscard]] static std::optional<SmallVector<std::size_t>>
 resolveQubitIndices(ValueRange qubits) {

As per coding guidelines, C++ code “MUST use Doxygen-style comments in C++ code.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp` around lines 47 - 81, The new
internal helpers lack Doxygen-style documentation in the implementation file, so
add brief `///` comments directly above `programQubitIndex` and
`resolveQubitIndices` in `CtrlOp.cpp`. Describe each helper’s purpose, inputs,
and return behavior at a high level so the C++ code follows the project’s
Doxygen comment requirement.

Source: Coding guidelines


364-396: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Cap the total participating wires before embedding.

Line 358 only limits controls, but Line 394 now builds a matrix over participating.size(). A case like 31 controls plus one target bypasses the guard and asks embedControlledUnitary to allocate a 32-wire embedding.

Proposed fix
   const auto controlQubits = resolveQubitIndices(getInputControls());
   const auto targetQubits = resolveQubitIndices(getInputTargets());
   if (!controlQubits || !targetQubits) {
     return std::nullopt;
   }
 
+  SmallVector<std::size_t> participating;
+  participating.append(*controlQubits);
+  participating.append(*targetQubits);
+  llvm::sort(participating);
+  participating.erase(llvm::unique(participating), participating.end());
+  if (participating.size() >= 32) {
+    llvm::reportFatalUsageError(
+        "Creating the unitary matrix for a CtrlOp with more than 31 "
+        "participating qubits is not supported due to memory constraints.");
+  }
+
   // Inner unitary on targets: one body op or a composed single-qubit sequence.
   std::optional<DynamicMatrix> targetMatrix;
@@
-  SmallVector<std::size_t> participating;
-  participating.append(*controlQubits);
-  participating.append(*targetQubits);
-  llvm::sort(participating);
-  participating.erase(llvm::unique(participating), participating.end());
-
   const auto toLocal = [&](const std::size_t wire) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp` around lines 364 - 396, The
controlled-unitary embedding in CtrlOp::getUnitaryMatrix now uses
participating.size(), so the total number of control plus target wires must be
capped before calling embedControlledUnitary. Update the existing size guard
near getInputControls/getInputTargets to reject cases where the combined
participating wires would exceed the allowed maximum, not just when
controlQubits is too large, and keep the check aligned with the local embedding
logic that builds participating and maps toLocal.
mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp (1)

235-244: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the full controlled matrix here.

This only checks 6 of the 16 entries, so a regression that introduces stray nonzeros or swaps an additional basis pair would still pass. Comparing against the full expected 4x4 matrix would make this regression test much harder to satisfy accidentally.

Suggested tightening
 TEST(DynamicMatrix, EmbedControlledUnitary) {
   // Controlled-X with control on wire 0 and target on wire 1.
   const auto controlledX =
       embedControlledUnitary(2, {0}, {1}, DynamicMatrix(pauliX()));
-  EXPECT_EQ(controlledX(0, 0), 1.0);
-  EXPECT_EQ(controlledX(1, 1), 0.0);
-  EXPECT_EQ(controlledX(1, 3), 1.0);
-  EXPECT_EQ(controlledX(2, 2), 1.0);
-  EXPECT_EQ(controlledX(3, 1), 1.0);
-  EXPECT_EQ(controlledX(3, 3), 0.0);
+  const auto expected = Matrix4x4::fromElements(1, 0, 0, 0, //
+                                                0, 0, 0, 1, //
+                                                0, 0, 1, 0, //
+                                                0, 1, 0, 0);
+  EXPECT_TRUE(controlledX.isApprox(expected));
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp` around lines 235 -
244, Tighten the DynamicMatrix.EmbedControlledUnitary test by asserting the
entire controlledX matrix instead of only a few entries. In
test_unitary_matrix.cpp within TEST(DynamicMatrix, EmbedControlledUnitary),
replace the partial EXPECT_EQ checks on controlledX with a full 4x4 expectation
against the correct controlled-X matrix so stray nonzeros or swapped basis pairs
are caught.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp`:
- Around line 47-81: The new internal helpers lack Doxygen-style documentation
in the implementation file, so add brief `///` comments directly above
`programQubitIndex` and `resolveQubitIndices` in `CtrlOp.cpp`. Describe each
helper’s purpose, inputs, and return behavior at a high level so the C++ code
follows the project’s Doxygen comment requirement.
- Around line 364-396: The controlled-unitary embedding in
CtrlOp::getUnitaryMatrix now uses participating.size(), so the total number of
control plus target wires must be capped before calling embedControlledUnitary.
Update the existing size guard near getInputControls/getInputTargets to reject
cases where the combined participating wires would exceed the allowed maximum,
not just when controlQubits is too large, and keep the check aligned with the
local embedding logic that builds participating and maps toLocal.

In `@mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp`:
- Around line 235-244: Tighten the DynamicMatrix.EmbedControlledUnitary test by
asserting the entire controlledX matrix instead of only a few entries. In
test_unitary_matrix.cpp within TEST(DynamicMatrix, EmbedControlledUnitary),
replace the partial EXPECT_EQ checks on controlledX with a full 4x4 expectation
against the correct controlled-X matrix so stray nonzeros or swapped basis pairs
are caught.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 596decd9-a052-4758-872e-ca576816162e

📥 Commits

Reviewing files that changed from the base of the PR and between 44e18e1 and f8b2d8e.

📒 Files selected for processing (5)
  • mlir/include/mlir/Dialect/QCO/Utils/Matrix.h
  • mlir/lib/Dialect/QCO/IR/Modifiers/CtrlOp.cpp
  • mlir/lib/Dialect/QCO/Utils/Matrix.cpp
  • mlir/unittests/Dialect/QCO/IR/test_qco_ir_matrix.cpp
  • mlir/unittests/Dialect/QCO/Utils/test_unitary_matrix.cpp

@burgholzer

Copy link
Copy Markdown
Member

Just gave this some thought as discussed offline today.
My conclusion is that this is not the way to go. In its current form this will not really work as the inference of the qubit index doesn't really work in my opinion.
Since qubits in QCO are linear types in all but very select cases, their defining operation will be another quantum operation and not a static qubit op or an extract.
As such, you would have to traverse the entire qubit value chain back to such an operation to potentially (!) infer the index. That's a big no-go complexity-wise.

As a result, I believe the other direction we discussed is a little more promising. That is, the methods of the individual operations just report a matrix representation in a canonical order. That's order is the one where the the control qubits are the MSQ's and the target qubits are the LSQ's, where we typically use big endian notation to write down bitstrings (q_n-1 ... q0).
It is then the responsibility of synthesis passes to construct and combine the functionality of matrices appropriately/in the right order.

The DD package always considers global/cirxuit-wide qubit indices; so it makes a distinction between cx(0,1) and cx(1,0) when constructing the DDs for these operations. In mqt-cc, the matrix would always be the cx(1,0) one. This also fits well with how Qiskit handles this: https://github.com/Qiskit/qiskit/blob/9ff63334c989c8c6e0ddd303a224ddbd907f0b6e/crates/circuit/src/gate_matrix.rs#L87

@simon1hofmann

Copy link
Copy Markdown
Contributor Author

Just gave this some thought as discussed offline today. My conclusion is that this is not the way to go. In its current form this will not really work as the inference of the qubit index doesn't really work in my opinion. Since qubits in QCO are linear types in all but very select cases, their defining operation will be another quantum operation and not a static qubit op or an extract. As such, you would have to traverse the entire qubit value chain back to such an operation to potentially (!) infer the index. That's a big no-go complexity-wise.

As a result, I believe the other direction we discussed is a little more promising. That is, the methods of the individual operations just report a matrix representation in a canonical order. That's order is the one where the the control qubits are the MSQ's and the target qubits are the LSQ's, where we typically use big endian notation to write down bitstrings (q_n-1 ... q0). It is then the responsibility of synthesis passes to construct and combine the functionality of matrices appropriately/in the right order.

The DD package always considers global/cirxuit-wide qubit indices; so it makes a distinction between cx(0,1) and cx(1,0) when constructing the DDs for these operations. In mqt-cc, the matrix would always be the cx(1,0) one. This also fits well with how Qiskit handles this: https://github.com/Qiskit/qiskit/blob/9ff63334c989c8c6e0ddd303a224ddbd907f0b6e/crates/circuit/src/gate_matrix.rs#L87

I also believe that is the way to go, closing this PR and the related Issue #1821.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c++ Anything related to C++ code MLIR Anything related to MLIR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

✨ Correct register-aware unitary embedding for CtrlOp::getUnitaryMatrix

2 participants